/* eslint no-prototype-builtins: 0 */
/**
 * jQuery-csv (jQuery Plugin)
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 *
 * Acknowledgements:
 * The original design and influence to implement this library as a jquery
 * plugin is influenced by jquery-json (http://code.google.com/p/jquery-json/).
 * If you're looking to use native JSON.Stringify but want additional backwards
 * compatibility for browsers that don't support it, I highly recommend you
 * check it out.
 *
 * A special thanks goes out to rwk@acm.org for providing a lot of valuable
 * feedback to the project including the core for the new FSM
 * (Finite State Machine) parsers. If you're looking for a stable TSV parser
 * be sure to take a look at jquery-tsv (http://code.google.com/p/jquery-tsv/).

 * For legal purposes I'll include the "NO WARRANTY EXPRESSED OR IMPLIED.
 * USE AT YOUR OWN RISK.". Which, in 'layman's terms' means, by using this
 * library you are accepting responsibility if it breaks your code.
 *
 * Legal jargon aside, I will do my best to provide a useful and stable core
 * that can effectively be built on.
 *
 * Copyrighted 2012 by Evan Plaice.
 */

 RegExp.escape = function (s) {
    return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
  };
  
  (function () {
    'use strict'
  
    let $
  
    // to keep backwards compatibility
    if (typeof jQuery !== 'undefined' && jQuery) {
      $ = jQuery
    } else {
      $ = {}
    }
  
    /**
     * jQuery.csv.defaults
     * Encapsulates the method paramater defaults for the CSV plugin module.
     */
  
    $.csv = {
      defaults: {
        separator: ',',
        delimiter: '"',
        headers: true
      },
  
      hooks: {
        castToScalar: function (value, state) {
          const hasDot = /\./
          if (isNaN(value)) {
            return value
          } else {
            if (hasDot.test(value)) {
              return parseFloat(value)
            } else {
              const integer = parseInt(value)
              if (isNaN(integer)) {
                return null
              } else {
                return integer
              }
            }
          }
        }
      },
  
      parsers: {
        parse: function (csv, options) {
          // cache settings
          const separator = options.separator
          const delimiter = options.delimiter
  
          // set initial state if it's missing
          if (!options.state.rowNum) {
            options.state.rowNum = 1
          }
          if (!options.state.colNum) {
            options.state.colNum = 1
          }
  
          // clear initial state
          const data = []
          let entry = []
          let state = 0
          let value = ''
          let exit = false
  
          function endOfEntry () {
            // reset the state
            state = 0
            value = ''
  
            // if 'start' hasn't been met, don't output
            if (options.start && options.state.rowNum < options.start) {
              // update global state
              entry = []
              options.state.rowNum++
              options.state.colNum = 1
              return
            }
  
            if (options.onParseEntry === undefined) {
              // onParseEntry hook not set
              data.push(entry)
            } else {
              const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook
              // false skips the row, configurable through a hook
              if (hookVal !== false) {
                data.push(hookVal)
              }
            }
            // console.log('entry:' + entry);
  
            // cleanup
            entry = []
  
            // if 'end' is met, stop parsing
            if (options.end && options.state.rowNum >= options.end) {
              exit = true
            }
  
            // update global state
            options.state.rowNum++
            options.state.colNum = 1
          }
  
          function endOfValue () {
            if (options.onParseValue === undefined) {
              // onParseValue hook not set
              entry.push(value)
            } else if (options.headers && options.state.rowNum === 1) {
              // don't onParseValue object headers
              entry.push(value)
            } else {
              const hook = options.onParseValue(value, options.state) // onParseValue Hook
              // false skips the row, configurable through a hook
              if (hook !== false) {
                entry.push(hook)
              }
            }
            // console.log('value:' + value);
            // reset the state
            value = ''
            state = 0
            // update global state
            options.state.colNum++
          }
  
          // escape regex-specific control chars
          const escSeparator = RegExp.escape(separator)
          const escDelimiter = RegExp.escape(delimiter)
  
          // compile the regEx str using the custom delimiter/separator
          let match = /(D|S|\r\n|\n|\r|[^DS\r\n]+)/
          let matchSrc = match.source
          matchSrc = matchSrc.replace(/S/g, escSeparator)
          matchSrc = matchSrc.replace(/D/g, escDelimiter)
          match = new RegExp(matchSrc, 'gm')
  
          // put on your fancy pants...
          // process control chars individually, use look-ahead on non-control chars
          csv.replace(match, function (m0) {
            if (exit) {
              return
            }
            switch (state) {
              // the start of a value
              case 0:
                // null last value
                if (m0 === separator) {
                  value += ''
                  endOfValue()
                  break
                }
                // opening delimiter
                if (m0 === delimiter) {
                  state = 1
                  break
                }
                // null last value
                if (/^(\r\n|\n|\r)$/.test(m0)) {
                  endOfValue()
                  endOfEntry()
                  break
                }
                // un-delimited value
                value += m0
                state = 3
                break
  
              // delimited input
              case 1:
                // second delimiter? check further
                if (m0 === delimiter) {
                  state = 2
                  break
                }
                // delimited data
                value += m0
                state = 1
                break
  
              // delimiter found in delimited input
              case 2:
                // escaped delimiter?
                if (m0 === delimiter) {
                  value += m0
                  state = 1
                  break
                }
                // null value
                if (m0 === separator) {
                  endOfValue()
                  break
                }
                // end of entry
                if (/^(\r\n|\n|\r)$/.test(m0)) {
                  endOfValue()
                  endOfEntry()
                  break
                }
                // broken paser?
                throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
  
              // un-delimited input
              case 3:
                // null last value
                if (m0 === separator) {
                  endOfValue()
                  break
                }
                // end of entry
                if (/^(\r\n|\n|\r)$/.test(m0)) {
                  endOfValue()
                  endOfEntry()
                  break
                }
                if (m0 === delimiter) {
                // non-compliant data
                  throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
                }
                // broken parser?
                throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
              default:
                // shenanigans
                throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
            }
            // console.log('val:' + m0 + ' state:' + state);
          })
  
          // submit the last entry
          // ignore null last line
          if (entry.length !== 0) {
            endOfValue()
            endOfEntry()
          }
  
          return data
        },
  
        // a csv-specific line splitter
        splitLines: function (csv, options) {
          if (!csv) {
            return undefined
          }
  
          options = options || {}
  
          // cache settings
          const separator = options.separator || $.csv.defaults.separator
          const delimiter = options.delimiter || $.csv.defaults.delimiter
  
          // set initial state if it's missing
          options.state = options.state || {}
          if (!options.state.rowNum) {
            options.state.rowNum = 1
          }
  
          // clear initial state
          const entries = []
          let state = 0
          let entry = ''
          let exit = false
  
          function endOfLine () {
            // reset the state
            state = 0
  
            // if 'start' hasn't been met, don't output
            if (options.start && options.state.rowNum < options.start) {
              // update global state
              entry = ''
              options.state.rowNum++
              return
            }
  
            if (options.onParseEntry === undefined) {
              // onParseEntry hook not set
              entries.push(entry)
            } else {
              const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook
              // false skips the row, configurable through a hook
              if (hookVal !== false) {
                entries.push(hookVal)
              }
            }
  
            // cleanup
            entry = ''
  
            // if 'end' is met, stop parsing
            if (options.end && options.state.rowNum >= options.end) {
              exit = true
            }
  
            // update global state
            options.state.rowNum++
          }
  
          // escape regex-specific control chars
          const escSeparator = RegExp.escape(separator)
          const escDelimiter = RegExp.escape(delimiter)
  
          // compile the regEx str using the custom delimiter/separator
          let match = /(D|S|\n|\r|[^DS\r\n]+)/
          let matchSrc = match.source
          matchSrc = matchSrc.replace(/S/g, escSeparator)
          matchSrc = matchSrc.replace(/D/g, escDelimiter)
          match = new RegExp(matchSrc, 'gm')
  
          // put on your fancy pants...
          // process control chars individually, use look-ahead on non-control chars
          csv.replace(match, function (m0) {
            if (exit) {
              return
            }
            switch (state) {
              // the start of a value/entry
              case 0:
                // null value
                if (m0 === separator) {
                  entry += m0
                  state = 0
                  break
                }
                // opening delimiter
                if (m0 === delimiter) {
                  entry += m0
                  state = 1
                  break
                }
                // end of line
                if (m0 === '\n') {
                  endOfLine()
                  break
                }
                // phantom carriage return
                if (/^\r$/.test(m0)) {
                  break
                }
                // un-delimit value
                entry += m0
                state = 3
                break
  
              // delimited input
              case 1:
                // second delimiter? check further
                if (m0 === delimiter) {
                  entry += m0
                  state = 2
                  break
                }
                // delimited data
                entry += m0
                state = 1
                break
  
              // delimiter found in delimited input
              case 2: {
                // escaped delimiter?
                const prevChar = entry.substr(entry.length - 1)
                if (m0 === delimiter && prevChar === delimiter) {
                  entry += m0
                  state = 1
                  break
                }
                // end of value
                if (m0 === separator) {
                  entry += m0
                  state = 0
                  break
                }
                // end of line
                if (m0 === '\n') {
                  endOfLine()
                  break
                }
                // phantom carriage return
                if (m0 === '\r') {
                  break
                }
                // broken paser?
                throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']')
              }
              // un-delimited input
              case 3:
                // null value
                if (m0 === separator) {
                  entry += m0
                  state = 0
                  break
                }
                // end of line
                if (m0 === '\n') {
                  endOfLine()
                  break
                }
                // phantom carriage return
                if (m0 === '\r') {
                  break
                }
                // non-compliant data
                if (m0 === delimiter) {
                  throw Error('CSVDataError: Illegal quote [Row:' + options.state.rowNum + ']')
                }
                // broken parser?
                throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']')
              default:
                // shenanigans
                throw Error('CSVDataError: Unknown state [Row:' + options.state.rowNum + ']')
            }
            // console.log('val:' + m0 + ' state:' + state);
          })
  
          // submit the last entry
          // ignore null last line
          if (entry !== '') {
            endOfLine()
          }
  
          return entries
        },
  
        // a csv entry parser
        parseEntry: function (csv, options) {
          // cache settings
          const separator = options.separator
          const delimiter = options.delimiter
  
          // set initial state if it's missing
          if (!options.state.rowNum) {
            options.state.rowNum = 1
          }
          if (!options.state.colNum) {
            options.state.colNum = 1
          }
  
          // clear initial state
          const entry = []
          let state = 0
          let value = ''
  
          function endOfValue () {
            if (options.onParseValue === undefined) {
              // onParseValue hook not set
              entry.push(value)
            } else {
              const hook = options.onParseValue(value, options.state) // onParseValue Hook
              // false skips the value, configurable through a hook
              if (hook !== false) {
                entry.push(hook)
              }
            }
            // reset the state
            value = ''
            state = 0
            // update global state
            options.state.colNum++
          }
  
          // checked for a cached regEx first
          if (!options.match) {
            // escape regex-specific control chars
            const escSeparator = RegExp.escape(separator)
            const escDelimiter = RegExp.escape(delimiter)
  
            // compile the regEx str using the custom delimiter/separator
            const match = /(D|S|\n|\r|[^DS\r\n]+)/
            let matchSrc = match.source
            matchSrc = matchSrc.replace(/S/g, escSeparator)
            matchSrc = matchSrc.replace(/D/g, escDelimiter)
            options.match = new RegExp(matchSrc, 'gm')
          }
  
          // put on your fancy pants...
          // process control chars individually, use look-ahead on non-control chars
          csv.replace(options.match, function (m0) {
            switch (state) {
              // the start of a value
              case 0:
                // null last value
                if (m0 === separator) {
                  value += ''
                  endOfValue()
                  break
                }
                // opening delimiter
                if (m0 === delimiter) {
                  state = 1
                  break
                }
                // skip un-delimited new-lines
                if (m0 === '\n' || m0 === '\r') {
                  break
                }
                // un-delimited value
                value += m0
                state = 3
                break
  
              // delimited input
              case 1:
                // second delimiter? check further
                if (m0 === delimiter) {
                  state = 2
                  break
                }
                // delimited data
                value += m0
                state = 1
                break
  
              // delimiter found in delimited input
              case 2:
                // escaped delimiter?
                if (m0 === delimiter) {
                  value += m0
                  state = 1
                  break
                }
                // null value
                if (m0 === separator) {
                  endOfValue()
                  break
                }
                // skip un-delimited new-lines
                if (m0 === '\n' || m0 === '\r') {
                  break
                }
                // broken paser?
                throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
  
              // un-delimited input
              case 3:
                // null last value
                if (m0 === separator) {
                  endOfValue()
                  break
                }
                // skip un-delimited new-lines
                if (m0 === '\n' || m0 === '\r') {
                  break
                }
                // non-compliant data
                if (m0 === delimiter) {
                  throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
                }
                // broken parser?
                throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
              default:
                // shenanigans
                throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
            }
            // console.log('val:' + m0 + ' state:' + state);
          })
  
          // submit the last value
          endOfValue()
  
          return entry
        }
      },
  
      helpers: {
  
        /**
         * $.csv.helpers.collectPropertyNames(objectsArray)
         * Collects all unique property names from all passed objects.
         *
         * @param {Array} objects Objects to collect properties from.
         *
         * Returns an array of property names (array will be empty,
         * if objects have no own properties).
         */
        collectPropertyNames: function (objects) {
          let o = []
          let propName = []
          const props = []
          for (o in objects) {
            for (propName in objects[o]) {
              if ((objects[o].hasOwnProperty(propName)) &&
                  (props.indexOf(propName) < 0) &&
                  (typeof objects[o][propName] !== 'function')) {
                props.push(propName)
              }
            }
          }
          return props
        }
      },
  
      /**
       * $.csv.toArray(csv)
       * Converts a CSV entry string to a javascript array.
       *
       * @param {Array} csv The string containing the CSV data.
       * @param {Object} [options] An object containing user-defined options.
       * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
       * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
       *
       * This method deals with simple CSV strings only. It's useful if you only
       * need to parse a single entry. If you need to parse more than one line,
       * use $.csv2Array instead.
       */
      toArray: function (csv, options, callback) {
        // if callback was passed to options swap callback with options
        if (options !== undefined && typeof (options) === 'function') {
          if (callback !== undefined) {
            return console.error('You cannot 3 arguments with the 2nd argument being a function')
          }
          callback = options
          options = {}
        }
  
        options = (options !== undefined ? options : {})
        const config = {}
        config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
        config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
        config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
        const state = (options.state !== undefined ? options.state : {})
  
        // setup
        options = {
          delimiter: config.delimiter,
          separator: config.separator,
          onParseEntry: options.onParseEntry,
          onParseValue: options.onParseValue,
          state: state
        }
  
        const entry = $.csv.parsers.parseEntry(csv, options)
  
        // push the value to a callback if one is defined
        if (!config.callback) {
          return entry
        } else {
          config.callback('', entry)
        }
      },
  
      /**
       * $.csv.toArrays(csv)
       * Converts a CSV string to a javascript array.
       *
       * @param {String} csv The string containing the raw CSV data.
       * @param {Object} [options] An object containing user-defined options.
       * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
       * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
       *
       * This method deals with multi-line CSV. The breakdown is simple. The first
       * dimension of the array represents the line (or entry/row) while the second
       * dimension contains the values (or values/columns).
       */
      toArrays: function (csv, options, callback) {
        // if callback was passed to options swap callback with options
        if (options !== undefined && typeof (options) === 'function') {
          if (callback !== undefined) {
            return console.error('You cannot 3 arguments with the 2nd argument being a function')
          }
          callback = options
          options = {}
        }
  
        options = (options !== undefined ? options : {})
        const config = {}
        config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
        config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
        config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
  
        // setup
        let data = []
        options = {
          delimiter: config.delimiter,
          separator: config.separator,
          onPreParse: options.onPreParse,
          onParseEntry: options.onParseEntry,
          onParseValue: options.onParseValue,
          onPostParse: options.onPostParse,
          start: options.start,
          end: options.end,
          state: {
            rowNum: 1,
            colNum: 1
          }
        }
  
        // onPreParse hook
        if (options.onPreParse !== undefined) {
          csv = options.onPreParse(csv, options.state)
        }
  
        // parse the data
        data = $.csv.parsers.parse(csv, options)
  
        // onPostParse hook
        if (options.onPostParse !== undefined) {
          data = options.onPostParse(data, options.state)
        }
  
        // push the value to a callback if one is defined
        if (!config.callback) {
          return data
        } else {
          config.callback('', data)
        }
      },
  
      /**
       * $.csv.toObjects(csv)
       * Converts a CSV string to a javascript object.
       * @param {String} csv The string containing the raw CSV data.
       * @param {Object} [options] An object containing user-defined options.
       * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
       * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
       * @param {Boolean} [headers] Indicates whether the data contains a header line. Defaults to true.
       *
       * This method deals with multi-line CSV strings. Where the headers line is
       * used as the key for each value per entry.
       */
      toObjects: function (csv, options, callback) {
        // if callback was passed to options swap callback with options
        if (options !== undefined && typeof (options) === 'function') {
          if (callback !== undefined) {
            return console.error('You cannot 3 arguments with the 2nd argument being a function')
          }
          callback = options
          options = {}
        }
  
        options = (options !== undefined ? options : {})
        const config = {}
        config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
        config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
        config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
        config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers
        options.start = 'start' in options ? options.start : 1
  
        // account for headers
        if (config.headers) {
          options.start++
        }
        if (options.end && config.headers) {
          options.end++
        }
  
        // setup
        let lines = []
        let data = []
  
        options = {
          delimiter: config.delimiter,
          separator: config.separator,
          onPreParse: options.onPreParse,
          onParseEntry: options.onParseEntry,
          onParseValue: options.onParseValue,
          onPostParse: options.onPostParse,
          start: options.start,
          end: options.end,
          state: {
            rowNum: 1,
            colNum: 1
          },
          match: false,
          transform: options.transform
        }
  
        // fetch the headers
        const headerOptions = {
          delimiter: config.delimiter,
          separator: config.separator,
          start: 1,
          end: 1,
          state: {
            rowNum: 1,
            colNum: 1
          },
          headers: true
        }
  
        // onPreParse hook
        if (options.onPreParse !== undefined) {
          csv = options.onPreParse(csv, options.state)
        }
  
        // parse the csv
        const headerLine = $.csv.parsers.splitLines(csv, headerOptions)
        const headers = $.csv.toArray(headerLine[0], headerOptions)
  
        // fetch the data
        lines = $.csv.parsers.splitLines(csv, options)
  
        // reset the state for re-use
        options.state.colNum = 1
        if (headers) {
          options.state.rowNum = 2
        } else {
          options.state.rowNum = 1
        }
  
        // convert data to objects
        for (let i = 0, len = lines.length; i < len; i++) {
          const entry = $.csv.toArray(lines[i], options)
          const object = {}
          for (let j = 0; j < headers.length; j++) {
            object[headers[j]] = entry[j]
          }
          if (options.transform !== undefined) {
            data.push(options.transform.call(undefined, object))
          } else {
            data.push(object)
          }
  
          // update row state
          options.state.rowNum++
        }
  
        // onPostParse hook
        if (options.onPostParse !== undefined) {
          data = options.onPostParse(data, options.state)
        }
  
        // push the value to a callback if one is defined
        if (!config.callback) {
          return data
        } else {
          config.callback('', data)
        }
      },
  
      /**
      * $.csv.fromArrays(arrays)
      * Converts a javascript array to a CSV String.
      *
      * @param {Array} arrays An array containing an array of CSV entries.
      * @param {Object} [options] An object containing user-defined options.
      * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
      * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
      *
      * This method generates a CSV file from an array of arrays (representing entries).
      */
      fromArrays: function (arrays, options, callback) {
        // if callback was passed to options swap callback with options
        if (options !== undefined && typeof (options) === 'function') {
          if (callback !== undefined) {
            return console.error('You cannot 3 arguments with the 2nd argument being a function')
          }
          callback = options
          options = {}
        }
  
        options = (options !== undefined ? options : {})
        const config = {}
        config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
        config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
        config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
  
        let output = ''
  
        for (let i = 0; i < arrays.length; i++) {
          const line = arrays[i]
          const lineValues = []
          for (let j = 0; j < line.length; j++) {
            let strValue = (line[j] === undefined || line[j] === null) ? '' : line[j].toString()
            if (strValue.indexOf(config.delimiter) > -1) {
              strValue = strValue.replace(new RegExp(config.delimiter, 'g'), config.delimiter + config.delimiter)
            }
  
            let escMatcher = '\n|\r|S|D'
            escMatcher = escMatcher.replace('S', config.separator)
            escMatcher = escMatcher.replace('D', config.delimiter)
  
            if (strValue.search(escMatcher) > -1) {
              strValue = config.delimiter + strValue + config.delimiter
            }
            lineValues.push(strValue)
          }
          output += lineValues.join(config.separator) + '\n'
        }
  
        // push the value to a callback if one is defined
        if (!config.callback) {
          return output
        } else {
          config.callback('', output)
        }
      },
  
      /**
       * $.csv.fromObjects(objects)
       * Converts a javascript dictionary to a CSV string.
       *
       * @param {Object} objects An array of objects containing the data.
       * @param {Object} [options] An object containing user-defined options.
       * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
       * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
       * @param {Character} [sortOrder] Sort order of columns (named after
       *   object properties). Use 'alpha' for alphabetic. Default is 'declare',
       *   which means, that properties will _probably_ appear in order they were
       *   declared for the object. But without any guarantee.
       * @param {Character or Array} [manualOrder] Manually order columns. May be
       * a strin in a same csv format as an output or an array of header names
       * (array items won't be parsed). All the properties, not present in
       * `manualOrder` will be appended to the end in accordance with `sortOrder`
       * option. So the `manualOrder` always takes preference, if present.
       *
       * This method generates a CSV file from an array of objects (name:value pairs).
       * It starts by detecting the headers and adding them as the first line of
       * the CSV file, followed by a structured dump of the data.
       */
      fromObjects: function (objects, options, callback) {
        // if callback was passed to options swap callback with options
        if (options !== undefined && typeof (options) === 'function') {
          if (callback !== undefined) {
            return console.error('You cannot 3 arguments with the 2nd argument being a function')
          }
          callback = options
          options = {}
        }
  
        options = (options !== undefined ? options : {})
        const config = {}
        config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
        config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
        config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
        config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers
        config.sortOrder = 'sortOrder' in options ? options.sortOrder : 'declare'
        config.manualOrder = 'manualOrder' in options ? options.manualOrder : []
        config.transform = options.transform
  
        if (typeof config.manualOrder === 'string') {
          config.manualOrder = $.csv.toArray(config.manualOrder, config)
        }
  
        if (config.transform !== undefined) {
          const origObjects = objects
          objects = []
  
          for (let i = 0; i < origObjects.length; i++) {
            objects.push(config.transform.call(undefined, origObjects[i]))
          }
        }
  
        let props = $.csv.helpers.collectPropertyNames(objects)
  
        if (config.sortOrder === 'alpha') {
          props.sort()
        }
  
        if (config.manualOrder.length > 0) {
          const propsManual = [].concat(config.manualOrder)
  
          for (let p = 0; p < props.length; p++) {
            if (propsManual.indexOf(props[p]) < 0) {
              propsManual.push(props[p])
            }
          }
          props = propsManual
        }
  
        let line
        const output = []
        let propName
        if (config.headers) {
          output.push(props)
        }
  
        for (let o = 0; o < objects.length; o++) {
          line = []
          for (let p = 0; p < props.length; p++) {
            propName = props[p]
            if (propName in objects[o] && typeof objects[o][propName] !== 'function') {
              line.push(objects[o][propName])
            } else {
              line.push('')
            }
          }
          output.push(line)
        }
  
        // push the value to a callback if one is defined
        return $.csv.fromArrays(output, options, config.callback)
      }
    }
  
    // Maintenance code to maintain backward-compatibility
    // Will be removed in release 1.0
    $.csvEntry2Array = $.csv.toArray
    $.csv2Array = $.csv.toArrays
    $.csv2Dictionary = $.csv.toObjects
  
    // CommonJS module is defined
    if (typeof module !== 'undefined' && module.exports) {
      module.exports = $.csv
    }
  }).call(this)